﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using ExtractItAll.Core;
using SevenZip;

namespace ExtractItAll.Extractors
{
  public class SevenZipSharpExtractor : BaseExtractor
  {
    #region Constants

    private static readonly string SevenZipDllPath = Path.Combine(
      Application.StartupPath,
      "7z.dll");

    private static readonly string SevenZip64DllPath = Path.Combine(
      Application.StartupPath,
      "7z64.dll");

    #endregion

    #region Fields

    private bool _failure;
    private ProgressInformation _info;

    #endregion

    #region Properties

    public override string InfoText
    {
      get
      {
        FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(GetSevenZipDllPath());

        return String.Format("7ZipExtractor ({0} v{1})",
                             versionInfo.ProductName,
                             versionInfo.FileVersion);
      }
    }

    public override int Priority
    {
      get { return 1; }
    }

    private string CurrentPassword { get; set; }

    #endregion

    private static string GetSevenZipDllPath()
    {
      bool is64BitProcess = (IntPtr.Size == 8);

      if (is64BitProcess)
        return SevenZip64DllPath;
      else
        return SevenZipDllPath;
    }

    public override bool IsArchiveFormatSupported(ArchiveFormat archiveFormat)
    {
      switch (archiveFormat)
      {
        case ArchiveFormat.SevenZip:
          return true;
        case ArchiveFormat.Zip:
          return true;
        case ArchiveFormat.Rar:
          return true;
        default:
          return false;
      }
    }

    public override bool Initialize(ArchiveItem archive)
    {
      if (archive == null) throw new ArgumentNullException("archive");
      Archive = archive;

      if (!File.Exists(GetSevenZipDllPath()))
      {
        Message = "File not found: " + GetSevenZipDllPath();
        return false;
      }

      SevenZipBase.SetLibraryPath(GetSevenZipDllPath());

      return true;
    }

    public override bool Extract(string password)
    {
      SevenZipExtractor extr;

      _failure = true;
      if (String.IsNullOrEmpty(password))
      {
        CurrentPassword = null;
        extr = new SevenZipExtractor(Archive.FileName);
      }
      else
      {
        CurrentPassword = password;
        extr = new SevenZipExtractor(Archive.FileName, CurrentPassword);
      }

      // check if password is correct
      try
      {
        uint i = extr.FilesCount;
      }
      catch (SevenZipArchiveException)
      {
        extr.Dispose();
        return false;
      }


      extr.Extracting += extr_Extracting;
      extr.FileExtractionStarted += extr_FileExtractionStarted;
      extr.FileExists += extr_FileExists;
      extr.ExtractionFinished += extr_ExtractionFinished;


      // check if path length is okay
      string extrPath = DestinationDir;
      var archiveFileData = extr.ArchiveFileData;
      int maxPathLength = archiveFileData.Max(f => f.FileName.Length);
      if (extrPath.Length + maxPathLength > 248)
      {
        string newDir = Path.Combine(Path.GetPathRoot(extrPath), "_EiA");
        if (Directory.Exists(newDir))
        {
          int counter = 1;
          string strTemp = newDir;
          while (Directory.Exists(strTemp))
          {
            strTemp = newDir + counter;
            counter++;
          }
          newDir = strTemp;
        }

        MessageBox.Show(String.Format(
                          "Path is too long for extraction: {1}{0}Max path length: {2}{0}{0}Using following path for this file:{3}",
                          Environment.NewLine,
                          extrPath,
                          248 - maxPathLength,
                          newDir
                          ), "Path too long", MessageBoxButtons.OK, MessageBoxIcon.Warning);

        extrPath = newDir;
      }

      Directory.CreateDirectory(extrPath);
      try
      {
        // if password will be verified after file has been extracted,
        // try to search for a small file first to speedup password detection
        if (Program.Settings.TrySmallFileFirst)
        {
          IEnumerable<ArchiveFileInfo> fileInfos = archiveFileData.Where(f =>
            f.IsDirectory == false &&
            f.Size > 0
            ).OrderBy(f => f.Size);

          if (fileInfos.Count() > 0)
          {
            ArchiveFileInfo info = fileInfos.First();

            if (info.Size <= 5 * 1000 * 1000)
            {
              extr.ExtractFile(info.Index, new StreamWriter(Path.GetTempFileName()).BaseStream);
            }
          }
        }

        extr.ExtractArchive(extrPath);
      }
      // catch different exceptions, which occur if no password or wrong password is set
      catch (SevenZipArchiveException exception)
      {
        _failure = true;
      }
      catch (ExtractionFailedException exception)
      {
        _failure = true;
      }
      catch (AccessViolationException exception)
      {
        _failure = true;
      }
      catch (IOException exception)
      {
        if (exception.Message.StartsWith("The volume ") &&
          exception.Message.EndsWith(" was not found. Extraction is impossible."))
        {
          Message = exception.Message;
          Canceled = true;
        }

        _failure = true;
      }
      catch (SevenZipException exception)
      {
        _failure = true;
      }
      catch (Exception exception)
      {
        _failure = true;
      }


      extr.Dispose();
      return !_failure;
    }

    public override void Close()
    {
      //Marshal.ReleaseComObject(InArchive);
    }


    private void extr_ExtractionFinished(object sender, EventArgs e)
    {
      //this.Invoke(new SetNoArgsDelegate(() =>
      //{
      //  pb_ExtractWork.Style = ProgressBarStyle.Blocks;
      //  pb_ExtractProgress.Value = 0;
      //  l_ExtractProgress.Text = "Finished";
      //}));
      _failure = false;

      _info.Completed = 100;
      Program.Tasks.RaiseArchiveProgressChanged(_info);
    }

    private void extr_FileExists(object sender, FileOverwriteEventArgs e)
    {
      //tb_Messages.Invoke(new SetOverwriteDelegate((args) =>
      //{
      //  tb_Messages.Text += String.Format("Warning: \"{0}\" already exists; overwritten\r\n", args.FileName);
      //}), e);
    }

    private void extr_FileExtractionStarted(object sender, FileInfoEventArgs e)
    {
      //l_ExtractProgress.Invoke(new SetInfoDelegate((args) =>
      //{
      //  l_ExtractProgress.Text = String.Format("Extracting \"{0}\"", args.FileInfo.FileName);
      //}), e);

      _info.Completed = 0;
      _info.Total = 100;
      _info.CurrentItem = Path.GetFileName(Archive.FileName);
      Program.Tasks.RaiseArchiveProgressChanged(_info);
    }

    private void extr_Extracting(object sender, ProgressEventArgs e)
    {
      //pb_ExtractProgress.Invoke(new SetProgressDelegate((args) =>
      //{
      //  pb_ExtractProgress.Increment(args.PercentDelta);
      //}), e);

      _info.Completed = e.PercentDone;
      Program.Tasks.RaiseArchiveProgressChanged(_info);
    }

    #region Nested type: SetFileNameDelegate

    private delegate void SetFileNameDelegate(FileNameEventArgs args);

    #endregion

    #region Nested type: SetInfoDelegate

    private delegate void SetInfoDelegate(FileInfoEventArgs args);

    #endregion

    #region Nested type: SetNoArgsDelegate

    private delegate void SetNoArgsDelegate();

    #endregion

    #region Nested type: SetOverwriteDelegate

    private delegate void SetOverwriteDelegate(FileOverwriteEventArgs args);

    #endregion

    #region Nested type: SetProgressDelegate

    private delegate void SetProgressDelegate(ProgressEventArgs args);

    #endregion

    #region Nested type: SetSettings

    private delegate void SetSettings(SevenZipCompressor compr);

    #endregion
  }
}